package com.yunzhou.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import com.yunzhou.dto.ESForumArticleDTO;
import com.yunzhou.dto.ForumArticleDTO;
import com.yunzhou.entity.ForumArticle;
import com.yunzhou.mapper.ForumArticleMapper;
import com.yunzhou.properties.ElasticSearchProperties;
import com.yunzhou.service.ElasticSearchService;
import jakarta.annotation.Resource;
import org.apache.http.HttpHost;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
public class ElasticSearchServiceImpl implements ElasticSearchService {

    @Resource
    private RestHighLevelClient client;
    @Resource
    private ForumArticleMapper forumArticleMapper;

    private static final String ARTICLE_MAPPING_TEMPLATE = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"articleId\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"boardId\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"boardName\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"parentBoardId\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"parentBoardName\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"userId\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"nickname\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"user_ip_address\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"title\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\", \n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"cover\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"summary\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\", \n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"postTime\":{\n" +
            "        \"type\": \"date\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"lastUpdateTime\":{\n" +
            "        \"type\": \"date\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"readCount\":{\n" +
            "        \"type\": \"integer\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"likeCount\":{\n" +
            "        \"type\": \"integer\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"commentCount\":{\n" +
            "        \"type\": \"integer\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"topType\":{\n" +
            "        \"type\": \"byte\",\n" +
            "        \"index\": true\n" +
            "      },\n" +
            "      \"status\":{\n" +
            "        \"type\": \"byte\",\n" +
            "        \"index\": true\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";

    /*
    * 执行顺序说明
            这些方法的执行顺序大致如下：
            @PostConstruct方法
            CommandLineRunner/ApplicationRunner实现
            ApplicationReadyEvent事件监听器

            选择哪种方式取决于你的具体需求：
            如果需要访问命令行参数，使用CommandLineRunner或ApplicationRunner
            如果只是想在Bean初始化时执行代码，使用@PostConstruct
            如果需要确保所有Bean都完全初始化后再执行，使用ApplicationListener或ApplicationReadyEvent

       注意避免在启动代码中执行耗时操作，以免影响应用启动速度。
    * */
    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady() throws IOException {
        // 如果需要确保所有Bean都完全初始化后再执行，使用ApplicationListener或ApplicationReadyEvent
        // 在这里编写启动时要执行的代码
        System.out.println("应用完全启动后执行这段代码");

        // 0、检查文章索引库是否存在
//        if (!getArticleIndex()) { // 如果文章索引库不存在则创建文章索引库,并上传数据到ES
//            // 1.创建文章索引库
//            createArticleIndex();
//            // 2.将MySQL数据库中的文章数据上传到ES
//            loadArticles();
//            System.out.println("文章数据上传完毕~");
//        }

        // 如果索引库存在，则删除，否则索引库不存在，也不必删除了
        if (getArticleIndex()) {
            // 0.删除文章索引库
            deleteArticleIndex();
        }
        // 1.创建文章索引库
        createArticleIndex();
        // 2.将MySQL数据库中的文章数据上传到ES
        loadArticles();
        System.out.println("文章数据上传完毕~");

    }

    // 0、检查文章索引库是否存在
    private boolean getArticleIndex() throws IOException {
        // 1.创建Request对象
        GetIndexRequest request = new GetIndexRequest("yunzhou_aritcle");
        // 2.发送请求
        return client.indices().exists(request, RequestOptions.DEFAULT);
    }

    // 0、删除文章索引库
    private void deleteArticleIndex() throws IOException {
        // 1.创建Request对象
        DeleteIndexRequest request = new DeleteIndexRequest("yunzhou_aritcle");
        // 2.发送请求
        client.indices().delete(request, RequestOptions.DEFAULT);
    }

    // 1.创建文章索引库
    private void createArticleIndex() throws IOException {
        // 1.创建Request对象
        CreateIndexRequest request = new CreateIndexRequest("yunzhou_aritcle");
        // 2.准备请求参数
        request.source(ARTICLE_MAPPING_TEMPLATE, XContentType.JSON);
        // 3.发送请求
        client.indices().create(request, RequestOptions.DEFAULT);
    }

    // 2.将MySQL数据库中的文章数据上传到ES
    private void loadArticles() throws IOException {
        // 分页查询商品数据
        int pageNum = 1;
        int pageSize = 1000;
        while (true) {
            List<ForumArticle> itemList = forumArticleMapper.getForumArticleList(
                    null,null,null,null,null,pageNum, pageSize);
            // 非空校验
            if (itemList.isEmpty()) {
                return;
            }
            System.out.println("当前页数：" + pageNum);
            System.out.println("当前size：" + pageSize);

            // 1.创建Request
            BulkRequest request = new BulkRequest("yunzhou_aritcle");
            // 2.准备参数，添加多个新增的Request
            for (ForumArticle item : itemList) {
                // 2.1.转换为文档类型ItemDTO
                ESForumArticleDTO esForumArticleDTO = BeanUtil.copyProperties(item, ESForumArticleDTO.class);
                // 2.2.创建新增文档的Request对象
                request.add(new IndexRequest()
                        .id(esForumArticleDTO.getArticleId())
                        .source(JSONUtil.toJsonStr(esForumArticleDTO), XContentType.JSON));
            }
            // 3.发送请求
            client.bulk(request, RequestOptions.DEFAULT);

            // 翻页
            pageNum++;
        }
    }

    // 3.从ElasticSearch中获取文章列表（不需要根据文章标题搜素，一进入网站就显示的文章）
    @Override
    public List<ESForumArticleDTO> getForumArticleList(Integer orderType, Integer pageNum, Integer pageSize) throws IOException {
        // 1.创建Request
        SearchRequest request = new SearchRequest("yunzhou_aritcle");

        // 2.组织请求参数
        // 2.1.query条件
        request.source().query(QueryBuilders.matchAllQuery());
        // 2.2.排序
        if (orderType == 0){ // 综合排序
            request.source().sort("topType", SortOrder.DESC);
            request.source().sort("likeCount", SortOrder.DESC);
            request.source().sort("commentCount", SortOrder.DESC);
            request.source().sort("readCount", SortOrder.DESC);
            request.source().sort("postTime", SortOrder.DESC);
        } else if (orderType == 1){ // 最新优先
            request.source().sort("postTime", SortOrder.DESC);
        }
        // 2.3 分页
        request.source().from((pageNum - 1) * pageSize).size(pageSize);
        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        // 4.解析响应
        List<ESForumArticleDTO> forumArticleList = handleResponse(response);

        // 关闭连接
        // client.close();

        // 返回结果
        return forumArticleList;
    }

    // 4.根据文章标题 从ElasticSearch中查询文章
    public List<ESForumArticleDTO> getForumArticleListByTitle(String title, Integer orderType, Integer pageNum, Integer pageSize) throws IOException {
        // 1.创建Request
        SearchRequest request = new SearchRequest("yunzhou_aritcle");

        // 2.组织请求参数
        // 2.1.query条件
        // request.source().query(QueryBuilders.matchPhraseQuery("title", title));
        request.source().query(QueryBuilders.multiMatchQuery(
                                    title, "title", "summary"));

        // 2.2.排序
        if (orderType == 0){ // 综合排序
            request.source().sort("topType", SortOrder.DESC);
            request.source().sort("likeCount", SortOrder.DESC);
            request.source().sort("commentCount", SortOrder.DESC);
            request.source().sort("readCount", SortOrder.DESC);
            request.source().sort("postTime", SortOrder.DESC);
//        } else if (orderType == 1){ // 最新优先
        } else { // 最新优先
            request.source().sort("postTime", SortOrder.DESC);
        }

        // 2.3.高亮条件
        request.source().highlighter(
                SearchSourceBuilder.highlight()
                        //.field("title") // 高亮属性值
                        //.field("summary")
//                        .requireFieldMatch(false) // 忽略大小写
//                        .field(new HighlightBuilder.Field("title").highlighterType("unified"))
//                        .field(new HighlightBuilder.Field("summary").highlighterType("unified"))
//                        .preTags("<em>") // 高亮标签
//                        .postTags("</em>")
                        .field("title") // 高亮属性值
                        .field("summary")
                        .preTags("<em>") // 高亮标签
                        .postTags("</em>")
        );

        // 2.4 分页
        request.source().from((pageNum - 1) * pageSize).size(pageSize);

        // 3.发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        // 4.解析响应
        List<ESForumArticleDTO> forumArticleList = handleResponse(response);

        // 关闭连接
        // client.close();

        // 返回结果
        return forumArticleList;
    }

    // 5、上传单个文章到ES(新增文档/全量修改文档)
    public void addForumArticleDocument(ForumArticle forumArticle) throws IOException {
        // 1.转换为文档类型（hutool包）
        ESForumArticleDTO esForumArticleDTO = BeanUtil.copyProperties(forumArticle, ESForumArticleDTO.class);
        // 2.将ItemDTO转json
        String doc = JSONUtil.toJsonStr(esForumArticleDTO);

        // 1.准备Request对象
        IndexRequest request = new IndexRequest("yunzhou_aritcle").id(esForumArticleDTO.getArticleId());
        // 2.准备Json文档
        request.source(doc, XContentType.JSON);
        // 3.发送请求
        client.index(request, RequestOptions.DEFAULT);
    }

    // 结果解析, 封装为函数
    private List<ESForumArticleDTO> handleResponse(SearchResponse response) {
        // 准备返回的结果
        List<ESForumArticleDTO> list = new ArrayList<>();

        SearchHits searchHits = response.getHits();
        // 1.获取总条数
        long total = searchHits.getTotalHits().value;
        System.out.println("共搜索到" + total + "条数据");
        // 2.遍历结果数组
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            // 3.得到_source，也就是原始json文档
            String source = hit.getSourceAsString();
            // 4.反序列化
            ESForumArticleDTO item = JSONUtil.toBean(source, ESForumArticleDTO.class);
            // 5.获取高亮结果
            Map<String, HighlightField> hfs = hit.getHighlightFields();
            if (hfs != null && !hfs.isEmpty()) {
                // 5.1.有高亮结果，获取title的高亮结果
                HighlightField hf = hfs.get("title");
                if (hf != null) {
                    // 5.2.获取第一个高亮结果片段，就是文章名称的高亮值
                    String hfTitle = hf.getFragments()[0].string();
                    // 5.3.覆盖非高亮结果
                    item.setTitle(hfTitle);
                }
                // summary同理
                HighlightField hf2 = hfs.get("summary");
                if (hf2 != null) {
                    // 5.2.获取第一个高亮结果片段，就是文章名称的高亮值
                    String hfSummary = hf2.getFragments()[0].string();
                    // 5.3.覆盖非高亮结果
                    item.setSummary(hfSummary);
                }
            }
            // System.out.println(item);

            // 添加到结果集
            list.add(item);
        }

        return list;
    }
}
